Expand Visualizer Tests and Patch Visualizer Bugs#5103
Expand Visualizer Tests and Patch Visualizer Bugs#5103matthewtrepte wants to merge 44 commits intoisaac-sim:developfrom
Conversation
Greptile SummaryThis PR expands visualizer test coverage with Cartpole integration tests (non-black frame and motion checks), adds regression tests from #5056, and fixes several bugs: the Newton viewer "Pause/Resume" button labels, stale RTX images after resets, Rerun launching without KitVisualizer, Kit headless recording, and overlapping
Confidence Score: 5/5Safe to merge; all findings are P2 style/UX concerns with no correctness or data-integrity issues. No P0 or P1 issues found. The is_training_paused()/playSimulations interaction was already flagged in a prior thread. Remaining items — missing render_product cleanup, browser-open skipped for new Rerun server, missing SimulationContext guard at test start, and missing CHANGELOG/version bump — are all P2 quality concerns that do not affect runtime correctness. source/isaaclab_visualizers/test/test_visualizer_cartpole_integration.py (test isolation and resource cleanup), source/isaaclab_visualizers/isaaclab_visualizers/kit/kit_visualizer.py (is_training_paused flag overlap) Important Files Changed
Sequence DiagramsequenceDiagram
participant Env as DirectRLEnv
participant SC as SimulationContext
participant PM as PhysicsManager
participant KitViz as KitVisualizer
participant RTX as ensure_rtx_render_update()
participant App as omni.kit.app
Env->>SC: step(render=True)
SC->>PM: step()
SC->>SC: render()
SC->>PM: pre_render()
SC->>SC: update_visualizers(dt)
SC->>KitViz: is_training_paused()?
KitViz-->>SC: reads /app/player/playSimulations
SC->>KitViz: step(dt)
KitViz->>App: set playSimulations=False
KitViz->>App: app.update()
KitViz->>App: set playSimulations=True
SC->>PM: after_visualizers_render()
Note over PM,RTX: PhysxManager calls ensure_rtx_render_update()
RTX->>SC: check any viz.pumps_app_update()?
alt KitVisualizer active
RTX-->>PM: skip - KitViz already pumped
else No KitVisualizer
RTX->>App: set playSimulations=False
RTX->>App: app.update()
RTX->>App: set playSimulations=True
end
Reviews (2): Last reviewed commit: "Merge branch 'develop' into mtrepte/upda..." | Re-trigger Greptile |
| "torch>=2.10", | ||
| "onnx>=1.18.0", # 1.16.2 throws access violation on Windows | ||
| "prettytable==3.3.0", | ||
| "munch", |
There was a problem hiding this comment.
munch dependency has no version constraint
All other dependencies in this file are pinned or have minimum version bounds (e.g. torch>=2.10, prettytable==3.3.0). An unpinned munch can silently pull in a breaking release. Consider adding a lower bound that has been verified to work:
| "munch", | |
| "munch>=4.0.0", |
| try: | ||
| import omni.kit.app | ||
|
|
||
| from isaaclab.app.settings_manager import get_settings_manager | ||
|
|
||
| app = omni.kit.app.get_app() | ||
| if app is not None and app.is_running(): | ||
| # Keep app pumping for viewport/UI updates only. | ||
| # Simulation stepping is owned by SimulationContext. | ||
| settings = get_settings_manager() | ||
| settings.set_bool("/app/player/playSimulations", False) | ||
| # Pump Kit/UI events only; SimulationContext owns physics stepping. | ||
| app.update() | ||
| settings.set_bool("/app/player/playSimulations", True) | ||
| except (ImportError, AttributeError) as exc: | ||
| logger.debug("[KitVisualizer] App update skipped: %s", exc) |
There was a problem hiding this comment.
Physics double-stepping risk without the old
playSimulations guard
The removed code blocked Kit from advancing physics during app.update() by temporarily setting /app/player/playSimulations to False. The guard was clearly broken — step() toggled the flag back to True immediately, so the old is_training_paused() could never observe False — but removing it entirely means Kit is now free to tick physics during app.update() when the timeline is in "playing" state, which could cause double-stepping alongside SimulationContext.
In practice Isaac Sim's app.update() should not independently step physics (physics ownership is expected to live in SimulationContext), and the new is_training_paused() correctly checks the timeline transport state rather than the now-gone setting. However, if a regression appears where physics advances twice per simulation step, this is the first place to investigate.
It would be worth adding a short comment here explaining why the guard was removed (it was effectively a no-op), so future readers don't re-introduce it.
There was a problem hiding this comment.
🤖 Isaac Lab Review Bot — PR #5103
Summary
This PR addresses two visualizer bugs and expands test coverage with non-black-frame integration tests. The changes span 5 files:
simulation_context.py— Addsviz.step(0.0)when rendering is paused, keeping the visualizer event loop responsive so UI controls (e.g. "Resume Rendering") remain interactive.setup.py— Addsmunchas a dependency for the Newton visualizer.test_simulation_context_visualizers.py— Adds a dedicated unit test for the rendering-paused fix and updates an existing test assertion.kit_visualizer.py— Rewritesstep()to usecarb.settingsdirectly (replacingget_settings_manager), and conditionally skipsapp.update()whenplaySimulationsis already falsy. Rewritesis_training_paused()to useomni.timelineinstead of the settings flag.test_visualizer_smoke_logs.py— Adds ~174 lines of integration test infrastructure and two new parametrized tests (test_kit_visualizer_non_black_viewport_frame,test_newton_visualizer_non_black_viewer_frame) that load a Cartpole camera scene and assert non-black rendered output.
Design Assessment
Rendering-paused fix (simulation_context.py): ✅ Sound approach. Calling viz.step(0.0) with zero dt while rendering is paused keeps event loops responsive without advancing simulation time. The fix targets the root cause — previously the continue statement completely skipped the visualizer, leaving its UI unresponsive. The zero-dt convention is already established for the training-pause loop on the adjacent line.
Kit visualizer step() rewrite: ✅ Good improvement. Replacing get_settings_manager() with direct carb.settings access removes an unnecessary abstraction layer. The conditional should_restore logic avoids the previous bug where app.update() was unconditionally called even when simulations weren't playing, which caused the Play/Pause button to erroneously unpause the physics.
Kit visualizer is_training_paused() rewrite: ✅ Correct. Using omni.timeline transport state is semantically accurate — the old implementation checked the /app/player/playSimulations settings flag which could be transiently toggled by step() itself, creating false positives for training pause detection. Timeline transport state is the authoritative source.
Non-black-frame tests:
Architecture Impact
- Low risk. The
simulation_context.pychange is a one-line addition inside an existing code path. - The
kit_visualizer.pychanges replace one internal API (get_settings_manager) with another (carb.settings), reducing coupling. The timeline-based pause detection is more robust. - Adding
munchtosetup.pyis a new runtime dependency — this is appropriate if Newton visualizer requires it, but the version should ideally be pinned (see Finding #1).
Implementation Verdict
Approve with findings. The bug fixes are correct and well-targeted. The test infrastructure is solid. However, the new integration tests have CI reliability issues that should be addressed before merge.
Test Coverage
| Area | Coverage |
|---|---|
Rendering-paused viz.step(0.0) |
✅ Dedicated unit test + updated existing test assertion |
Kit step() conditional update |
carb.settings |
Kit is_training_paused() timeline |
|
| Non-black frame detection helpers | ✅ Exercised by integration tests |
| Non-black viewport tests | 🔴 3/3 new integration test parametrizations failing in CI |
CI Status
| Job | Status | Notes |
|---|---|---|
isaaclab_visualizers |
🔴 FAIL | 3 new tests fail: test_kit_visualizer_non_black_viewport_frame[newton] (CUDA OOM during render product allocation), test_newton_visualizer_non_black_viewer_frame[physx] and [newton] (CUDA illegal memory access during SimulationContext init — GPU state corrupted by prior test) |
isaaclab (core) [1-3/3] |
🔴 FAIL | Failures in test_non_headless_launch, test_first_frame_textured_rendering, test_ray_caster_camera — unrelated to this PR |
isaaclab_physx |
🔴 FAIL | Likely pre-existing, unrelated |
isaaclab_tasks [3/3] |
🔴 FAIL | Likely pre-existing, unrelated |
environments_training |
🔴 FAIL | Likely pre-existing, unrelated |
pre-commit |
✅ PASS | |
license-check |
✅ PASS |
Findings
Finding 1 — munch dependency is unpinned
File: source/isaaclab/setup.py, line 25
Severity: Low
Issue: munch is added without a version pin. Every other dependency in this section has an explicit version or minimum version constraint. An unpinned munch could cause unexpected breakage if a future major version introduces breaking API changes.
Suggestion: Pin to a minimum version, e.g. "munch>=4.0.0" or "munch==4.0.0".
Finding 2 — Kit step() skips app.update() when playSimulations is already falsy
File: source/isaaclab_visualizers/isaaclab_visualizers/kit/kit_visualizer.py, lines 108-119
Severity: Medium
Issue: The new logic only calls app.update() inside if should_restore, meaning when playSimulations is already False, app.update() is never called at all. The old code unconditionally called app.update() (after disabling playSimulations). This behavioral change means the Kit viewport will not pump UI events when simulation is already paused, which could prevent viewport refresh in paused-then-rendering scenarios.
# Current:
should_restore = bool(previous_play_simulations)
if should_restore:
settings.set_bool(play_simulations_path, False)
app.update()
if should_restore:
settings.set_bool(play_simulations_path, True)
# Consider instead:
should_restore = bool(previous_play_simulations)
if should_restore:
settings.set_bool(play_simulations_path, False)
app.update() # Always pump UI events
if should_restore:
settings.set_bool(play_simulations_path, True)Alternatively, if the intent is specifically to skip app.update() when paused, document this behavior with a comment explaining why.
Finding 3 — New integration tests are not resilient to GPU state corruption from prior tests
File: source/isaaclab_visualizers/test/test_visualizer_smoke_logs.py
Severity: Medium
Issue: The CI failures show that test_newton_visualizer_non_black_viewer_frame[physx] and [newton] fail with torch.AcceleratorError: CUDA error: an illegal memory access during SimulationContext.__init__ — this is GPU state corruption carried over from the preceding test_kit_visualizer_non_black_viewport_frame[newton] failure (CUDA OOM in render product allocation). Since all tests run in a single process (required by Kit app lifetime), a CUDA error in one test poisons all subsequent GPU operations.
Suggestion: Consider adding pytest.mark.order to run the non-black frame tests last, or adding a CUDA device reset/sync check at the start of each test. Alternatively, mark these as @pytest.mark.xfail(reason="Requires dedicated GPU resources") until the CI environment is stabilized, so they don't block the PR pipeline.
Finding 4 — PR description mentions a known TODO that is not addressed
Severity: Info
Issue: The PR body states: "TODO: fix Unpause behavior, currently body transforms remain stale after Unpausing... recent regression". This indicates a known remaining bug in the Kit visualizer's pause/unpause lifecycle. This is fine as a known limitation, but consider filing a tracking issue so it doesn't get lost.
Review by 🤖 Isaac Lab Review Bot — 4a42c18b
There was a problem hiding this comment.
🤖 Isaac Lab Review Bot — Follow-up
New commits (4a42c18→053adbd4): renamed camera_position/camera_target → eye/lookat across the codebase, simplified Kit step(), and merged develop.
One new issue found in the rename.
3dd079a to
5eca07a
Compare
There was a problem hiding this comment.
🤖 Isaac Lab Review Bot — PR #5103
Summary
This PR addresses multiple visualizer bugs (Newton pause button, Kit headless recording, rerun+newton launch, RTX stale images, wrist camera orientation), renames camera_position/camera_target to eye/lookat across the board, deprecates ViewerCfg overrides in favor of VisualizerCfg subclasses, and replaces the lightweight smoke tests with comprehensive cartpole-based integration tests that verify non-black frames and motion detection. The scope is significant — 24 files touching core simulation, env initialization, physics hooks, visualizer implementations, installation tooling, and test infrastructure.
Design Assessment
Mostly sound, with one architectural concern flagged by the maintainer.
The overall direction — dedicated visualizer cameras, deprecating ViewerCfg, decoupling Kit-specific logic from SimulationContext — is correct and moves toward cleaner separation. The after_visualizers_render() hook in PhysicsManager with the Kit-specific override in PhysxManager is a reasonable pattern to keep SimulationContext backend-agnostic.
However, has_active_visualizers() and can_render_rgb_array() in SimulationContext (simulation_context.py) directly query Isaac Sim settings paths (/isaaclab/visualizer/types). As maintainer @kellyguo11 noted, this introduces backend-specific coupling into what should be a backend-agnostic class. Consider moving these helpers to the PhysicsManager interface or making the setting path discovery pluggable.
Findings
🔴 Critical: _pending_camera_view not set in unit test helpers — CI failure — simulation_context.py:603 / test_simulation_context_visualizers.py
The _pending_camera_view attribute is initialized in SimulationContext.__init__(), but several unit tests create context objects via object.__new__(SimulationContext) (bypassing __init__). The _make_context_with_settings helper was updated to set _pending_camera_view = None and _render_generation = 0, but the simpler _make_context() helper was not. This causes the AttributeError that @kellyguo11 reported in CI:
E AttributeError: 'SimulationContext' object has no attribute '_pending_camera_view'
The _make_context() function (test line ~130) also needs _pending_camera_view = None and _render_generation = 0 set on the context object, just as _make_context_with_settings() does.
🔴 Critical: _resolve_cfg_camera_pose uses bare getattr without fallback — base_visualizer.py:196-198
eye = tuple(float(v) for v in getattr(self.cfg, "eye"))
lookat = tuple(float(v) for v in getattr(self.cfg, "lookat"))getattr(obj, name) without a default raises AttributeError if the attribute is missing — identical to self.cfg.eye. The getattr wrapping provides no safety benefit here and is misleading. If the intent is to support subclasses with different field names, add a default and handle the missing case. If eye/lookat are always present (they are, on VisualizerCfg), use direct attribute access for clarity:
eye = tuple(float(v) for v in self.cfg.eye)
lookat = tuple(float(v) for v in self.cfg.lookat)The del visualizer_name is also a code smell — if the parameter is unused, either drop it from the signature or document why it exists for subclass overrides.
🟡 Warning: _viewer_cfg_value_matches_default fragile for nested configclass fields — envs/common.py:22-29
The deprecation check in ViewerCfg.__post_init__ compares each field to its declared default. This works for simple types but will silently miss nested @configclass fields whose defaults are factory-constructed — f.default would be MISSING and f.default_factory is a dataclasses concept that @configclass may not always set identically. For now all ViewerCfg fields are simple scalars/tuples so this is safe, but the approach should be noted as fragile if fields are added later.
🟡 Warning: pump_kit_app_for_headless_video_render_if_needed catches all exceptions silently — isaac_rtx_renderer_utils.py:138-139
except Exception as exc:
logger.debug("[isaac_rtx] Skipping Kit app-loop pump in render(): %s", exc)A bare except Exception with only debug-level logging around ensure_isaac_rtx_render_update() means that genuine rendering failures (e.g., GPU resource exhaustion, CUDA errors) will be silently swallowed. At minimum, this should log at warning level so operators can diagnose headless video recording failures. The debug level is appropriate for expected non-Kit environments, but unexpected errors deserve more visibility.
🟡 Warning: Breaking change in camera config field names not documented in CHANGELOG — visualizer_cfg.py
Renaming camera_position→eye, camera_target→camera_target, camera_source→cam_source, camera_usd_path→cam_prim_path is a breaking change for any user with custom VisualizerCfg in their training scripts. The PR description mentions the rename but per Isaac Lab contributing guidelines, breaking changes must be documented in CHANGELOG.rst. No CHANGELOG update is included.
Additionally, the old → new error path changed from a warning + fallback (graceful) to a RuntimeError (crash) when cam_source='prim_path' fails to resolve. This is a stricter behavior change that should be called out:
# Before: warning + fallback to cfg camera
logger.warning("[NewtonVisualizer] camera_usd_path '%s' not found; using configured camera.", ...)
# After: hard crash
raise RuntimeError("[NewtonVisualizer] cam_source='prim_path' requires a resolvable camera prim path...")While fail-fast is generally better for misconfigurations, this is a behavior change for existing users whose USD camera paths resolve at runtime (e.g., deferred scene loading).
🟡 Warning: KitVisualizerCfg.create_viewport default changed from False to True — kit_visualizer_cfg.py
This is a silent behavior change: all KitVisualizerCfg() users who relied on the default create_viewport=False will now get a new viewport created automatically. Combined with the new validation that viewport_name must be non-empty when create_viewport=True, this could surprise existing users.
🔵 Suggestion: Prebundle repoint search across symlinks could be slow — cli/commands/install.py:358-374
The new prebundle discovery resolves symlinks and searches up to 6+ root directories with rglob("pip_prebundle"). On systems with large Omniverse caches (e.g., ~/.local/share/ov/data/exts with hundreds of extensions), this could be noticeably slow. Consider caching the result or adding a debug-level log of how many dirs were scanned and how long it took, to aid diagnosis of slow isaaclab install runs.
🔵 Suggestion: Test module-level simulation_app launch prevents test isolation — test_visualizer_cartpole_integration.py:25
simulation_app = AppLauncher(headless=True, enable_cameras=True).appLaunching the Kit app at module import time means all tests in this file share a single app instance. This is standard for Isaac Lab tests, but worth noting that test ordering could matter — a test that corrupts app state (e.g., a failed viewport creation) could cascade failures to subsequent tests.
Test Coverage
- Bug fixes: Partial regression coverage. The Newton pause button fix has a unit test (
test_update_visualizers_skips_zero_dt_for_paused_app_pumping_visualizer). The RTX stale image fix is exercised via the Kit viewport motion test. However, the headless recording fix and rerun+newton launch fix don't have dedicated regression tests — they're implicitly covered by the integration tests working, but a reintroduction of the bug might not be caught. - New integration tests are comprehensive. The cartpole-based tests verify non-black frames, frame-to-frame motion detection (early vs late captures), and log hygiene across 4 visualizer × 2 backend combinations. The motion threshold approach (channel diff ≥ 50, ≥ 100 differing pixels) is well-calibrated for catching frozen/stale rendering.
- Deleted smoke tests replaced adequately. The old
test_visualizer_smoke_logs.pytested initialization + stepping with a minimal env. The new tests use a real cartpole scene which is strictly more rigorous. - Gap: No test for
ViewerCfgdeprecation warning. The__post_init__deprecation path is only tested indirectly viatest_configclass.pywrapping an existing test inpytest.warns. A direct test thatViewerCfg(eye=(1,1,1))emits the warning whileViewerCfg()does not would be valuable.
CI Status
All checks passing ✅ (pre-commit, license-check, build wheel, docs, installation tests). Note: the CI failure @kellyguo11 reported (AttributeError: '_pending_camera_view') may have been from a prior commit — the current SHA adds the attribute in _make_context_with_settings but not in _make_context, so the issue likely persists for the test_non_explicit_create_failure_silently_logged test path.
Verdict
Significant concerns
The architecture and direction are right, but there are blocking issues: a CI-failing AttributeError from incomplete test helper updates, a breaking config rename without CHANGELOG documentation, and maintainer questions about SimulationContext backend coupling that need resolution. The new integration tests are high quality and a clear improvement. Recommend addressing the CI failure and maintainer feedback before merging.
| # | ||
| # SPDX-License-Identifier: BSD-3-Clause | ||
|
|
||
| """Kit / Omniverse viewport helpers (Isaac Sim specific). |
There was a problem hiding this comment.
does this belong in the renderers? or more for visualizers?
There was a problem hiding this comment.
🤖 Isaac Lab Review Bot — PR #5103
Summary
This PR delivers a substantial overhaul of the visualizer subsystem:
- Camera config rename:
camera_position/camera_target→eye/lookat,camera_source/camera_usd_path→cam_source/cam_prim_path— consistent API across all visualizers. - ViewerCfg deprecation:
__post_init__warning when non-default fields are overridden, migrating towardKitVisualizerCfg/NewtonVisualizerCfgas the sole camera config surface. - Bug fixes: Pause-rendering button in Newton, headless video recording in Kit, stale RTX image after resets, USD stage traversal safety, wrist camera orientation offset.
- New render lifecycle hooks:
after_visualizers_render()onPhysicsManager,render_generationcounter,_pending_camera_viewreplay. - Kit visualizer: Dedicated camera prim (
enable_visualizer_cam,visualizer_camera_prim_path,focal_length), viewport decoupling from sensor cameras. - Prebundle repointing: Expanded to Omniverse cache roots and new packages (
websockets,viser,imgui_bundle). - Tests: Replaced smoke test with comprehensive cartpole integration tests including non-black frame and motion assertions.
Overall this is a well-structured PR with clear separation of concerns. Inline comments below cover specific issues found.
Verdict
🟡 Good with minor issues — A few robustness and correctness items to address before merge (see inline comments).
| ) -> tuple[tuple[float, float, float], tuple[float, float, float]]: | ||
| """Resolve camera pose from cfg eye/lookat fields.""" | ||
| del visualizer_name | ||
| eye = tuple(float(v) for v in getattr(self.cfg, "eye")) |
There was a problem hiding this comment.
🟡 Warning: Using bare getattr(self.cfg, "eye") without a default will raise AttributeError if a subclass cfg doesn't define eye/lookat (same as direct attribute access). Since this is a base-class method meant to be inherited by all visualizers, consider defensive access with a fallback or an explicit check:
| eye = tuple(float(v) for v in getattr(self.cfg, "eye")) | |
| eye = tuple(float(v) for v in getattr(self.cfg, "eye", (7.5, 7.5, 7.5))) | |
| lookat = tuple(float(v) for v in getattr(self.cfg, "lookat", (0.0, 0.0, 0.0))) |
Alternatively, use self.cfg.eye directly (clearer intent) if all VisualizerCfg subclasses are guaranteed to have these fields — the getattr without a default gives no benefit over direct attribute access.
| self, visualizer_name: str | ||
| ) -> tuple[tuple[float, float, float], tuple[float, float, float]]: | ||
| """Resolve camera pose from cfg eye/lookat fields.""" | ||
| del visualizer_name |
There was a problem hiding this comment.
🟡 Suggestion: The visualizer_name parameter is accepted then immediately deleted. If it's reserved for future subclass overrides, a leading underscore or # noqa would better signal that intent. As-is, callers like NewtonVisualizer and RerunVisualizer pass a name string that is silently discarded, which could confuse future maintainers.
Consider either:
- Removing the parameter if no subclass uses it, or
- Using
_visualizer_namewith a comment explaining the future intent.
| # Replay any camera pose requested before visualizers were initialized. | ||
| pending = getattr(self, "_pending_camera_view", None) | ||
| if pending is not None: | ||
| eye, target = pending |
There was a problem hiding this comment.
🟡 Warning: _pending_camera_view is never cleared after replay. If initialize_visualizers() is called multiple times (e.g. on a re-init), the stale pending pose will be re-applied to the new set of visualizers. Consider clearing it after replay:
| eye, target = pending | |
| pending = getattr(self, "_pending_camera_view", None) | |
| if pending is not None: | |
| eye, target = pending | |
| for viz in self._visualizers: | |
| viz.set_camera_view(eye, target) | |
| self._pending_camera_view = None |
There was a problem hiding this comment.
do we want to enforce that initialize_visualizers only gets called once?
There was a problem hiding this comment.
Good question. There's already a guard at the top of the method (if self._visualizers: return) that makes repeated calls a no-op, which handles the common case well. That said, the guard is based on the list being non-empty — if initialization ran but all visualizers failed (leaving self._visualizers as []), a second call would re-run everything.
For robustness, an explicit _visualizers_initialized flag (checked/set at the top) would be the cleanest way to enforce single initialization regardless of outcome. But that's a broader design choice — the current early-return + clearing _pending_camera_view (which the author has already adopted ✅) is likely sufficient for the immediate scope of this PR.
| """Return whether any visualizer path is active for rendering/camera control.""" | ||
| return bool(self.get_setting("/isaaclab/visualizer/types")) | ||
|
|
||
| def can_render_rgb_array(self) -> bool: |
There was a problem hiding this comment.
🟢 Nit: can_render_rgb_array() is defined but never referenced in this PR. If it's intended as public API for downstream code (e.g. gymnasium render_mode="rgb_array" checks), consider adding a brief usage note in the docstring so readers know when to use it vs is_rendering.
| Path.home() / ".local" / "share" / "ov" / "data" / "exts" / "v2", | ||
| ): | ||
| if root.exists(): | ||
| candidate_roots.add(root) |
There was a problem hiding this comment.
🟡 Warning: rglob("pip_prebundle") on broad roots like ~/.local/share/ov/data/exts could be slow on machines with large Omniverse caches (thousands of extensions). This runs during isaaclab install so user-facing latency matters.
Consider adding a maxdepth-style guard (e.g. limit recursion depth) or at minimum logging the discovery time so users can diagnose slow installs:
import time
t0 = time.monotonic()
for root in candidate_roots:
prebundle_dirs.update(root.rglob("pip_prebundle"))
print_debug(f"Discovered {len(prebundle_dirs)} pip_prebundle dirs in {time.monotonic() - t0:.1f}s")| # In headless mode we keep the visualizer active but skip viewport/window setup. | ||
| self._viewport_window = None | ||
| self._viewport_api = None | ||
| self._controlled_camera_path = "/OmniverseKit_Persp" if self.cfg.enable_visualizer_cam else None |
There was a problem hiding this comment.
🟡 Question: In headless mode with enable_visualizer_cam=True, _controlled_camera_path is hardcoded to "/OmniverseKit_Persp". But the new design creates a dedicated camera prim at visualizer_camera_prim_path (default "/World/Cameras/KitVisualizerCamera") when a viewport API exists. This means headless and GUI modes target different camera prims, which could cause subtle behavioral differences (e.g. a sensor camera at /OmniverseKit_Persp getting its transform overwritten in headless).
Is the headless fallback to /OmniverseKit_Persp intentional? Should it also use visualizer_camera_prim_path for consistency?
| create_viewport: bool = False | ||
| create_viewport: bool = True | ||
| """Create new viewport with specified name and camera pose.""" | ||
|
|
There was a problem hiding this comment.
🟢 Nit — breaking change note: create_viewport changed from False to True as default. This is a behavioral change for existing users who instantiate KitVisualizerCfg() without explicit arguments — they'll now get a new viewport window by default. The PR description mentions this is a breaking change, but consider calling it out in the CHANGELOG entry and docstring.
| else: | ||
| continue | ||
| if not _viewer_cfg_value_matches_default(getattr(self, f.name), default_val): | ||
| differing.append(f.name) |
There was a problem hiding this comment.
🟡 Warning: The __post_init__ deprecation check fires on every ViewerCfg construction, including internal framework code that may use non-default values intentionally (e.g. task configs, test fixtures). The stacklevel=2 may not always point to user code — for configclass-decorated classes, the actual call stack through from_dict() or replace() can be deeper.
Consider:
- Using
stacklevel=3or computing it dynamically to ensure the warning points at the user's config file, not framework internals. - Adding a
_suppress_deprecationescape hatch for internal usage (e.g.ViewerCfg(_suppress_deprecation=True)).
|
|
||
| # launch Kit app | ||
| simulation_app = AppLauncher(headless=True, enable_cameras=True).app | ||
|
|
There was a problem hiding this comment.
🟢 Observation: AppLauncher(headless=True, enable_cameras=True) at module top-level means this file cannot be imported without side effects (Kit app launch). This is standard for Isaac Lab tests, but worth noting that it prevents running individual test functions in isolation via pytest -k without the full app startup. No action needed — just flagging for awareness.
|
are the changes in #5056 still required with this PR? |
i think its covered by this PR, and i added Pascal's unit tests here from his PR, and they pass perhjaps @michaellin6 can confirm |
There was a problem hiding this comment.
🤖 Isaac Lab Review Bot
Summary
This PR makes substantive improvements to the visualizer subsystem: renames camera_position/camera_target → eye/lookat across configs, fixes headless video recording by routing Kit app-loop pumping through PhysicsManager.after_visualizers_render(), adds a ViewerCfg deprecation path, decouples viewport camera control from visualizer broadcasting, and replaces the deleted smoke tests with comprehensive cartpole integration tests that verify non-black frames and motion detection.
The architecture changes are well-motivated — separating Kit-specific rendering from the backend-agnostic SimulationContext, adding render_generation to fix stale-frame dedup, and introducing pumps_app_update() to avoid double-pumping. A few issues are worth addressing before merge.
Design Assessment
Design is sound. The separation of Kit-specific rendering into isaaclab_physx (via kit_viewport_utils.py and after_visualizers_render) keeps SimulationContext backend-agnostic. The pumps_app_update() protocol is a clean way to prevent double-rendering. The ViewerCfg deprecation with __post_init__ field-diffing is thorough. The new integration tests are a significant quality improvement over the deleted smoke tests.
Minor concerns detailed in inline comments.
Test Coverage
- Bug fix regression tests: ✅
test_render_pumps_app_update_without_visualizerandtest_render_skips_app_update_when_visualizer_pumps_itdirectly cover the headless video fix from PR #5056 - Integration tests: ✅ Comprehensive cartpole integration tests cover Kit, Newton, Rerun, and Viser visualizers with non-black frame and motion-detection assertions
- Deprecation warning: ✅
test_config_update_via_constructorupdated to expectDeprecationWarning - Paused visualizer behavior: ✅
test_update_visualizers_skips_zero_dt_for_paused_app_pumping_visualizercovers the new paused-but-pumping path - Gap: No unit test for
ViewerCfg.__post_init__deprecation warning edge cases in isolation (e.g. list vs tuple equivalence in_viewer_cfg_value_matches_default)
CI Status
pre-commit and Build Wheel pass.
Verdict
Minor fixes needed
The approach is correct and well-architected. The inline comments identify a few issues — mostly around error handling robustness and a cross-module dependency concern. None are blocking correctness for the primary use cases.
| ) -> tuple[tuple[float, float, float], tuple[float, float, float]]: | ||
| """Resolve camera pose from cfg eye/lookat fields.""" | ||
| del visualizer_name | ||
| eye = tuple(float(v) for v in getattr(self.cfg, "eye")) |
There was a problem hiding this comment.
🔵 Suggestion: getattr(self.cfg, "eye") without a default is equivalent to self.cfg.eye — both raise AttributeError if missing. Direct attribute access is clearer and lets type checkers validate it.
| eye = tuple(float(v) for v in getattr(self.cfg, "eye")) | |
| eye = tuple(float(v) for v in self.cfg.eye) |
| """Resolve camera pose from cfg eye/lookat fields.""" | ||
| del visualizer_name | ||
| eye = tuple(float(v) for v in getattr(self.cfg, "eye")) | ||
| lookat = tuple(float(v) for v in getattr(self.cfg, "lookat")) |
There was a problem hiding this comment.
Same as above:
| lookat = tuple(float(v) for v in getattr(self.cfg, "lookat")) | |
| lookat = tuple(float(v) for v in self.cfg.lookat) |
| import isaacsim.core.utils.viewports as isaacsim_viewports | ||
|
|
||
| isaacsim_viewports.set_camera_view(eye=list(eye), target=list(target), camera_prim_path=str(camera_prim_path)) | ||
| except Exception as exc: |
There was a problem hiding this comment.
🟡 Warning: This bare except Exception silently downgrades all errors to DEBUG level. If the camera prim path is wrong or the viewport state is invalid, the user gets no visible feedback — the camera just silently stays in the wrong position.
Consider splitting expected vs unexpected failures:
| except Exception as exc: | |
| except (ImportError, ModuleNotFoundError) as exc: | |
| logger.debug("[kit_viewport] Renderer camera update skipped (no Kit): %s", exc) | |
| except Exception as exc: | |
| logger.warning("[kit_viewport] Renderer camera update failed: %s", exc) |
| try: | ||
| ensure_isaac_rtx_render_update() | ||
| except Exception as exc: | ||
| logger.debug("[isaac_rtx] Skipping Kit app-loop pump in render(): %s", exc) |
There was a problem hiding this comment.
🟡 Warning: Same concern — except Exception with logger.debug in the --video headless recording path means Fabric sync errors, USD context corruption, etc. would silently produce stale/black frames with no user-visible indication.
Since the user explicitly requested --video, a failure here directly impacts their output:
| logger.debug("[isaac_rtx] Skipping Kit app-loop pump in render(): %s", exc) | |
| logger.warning("[isaac_rtx] Kit app-loop pump failed in render() — video frames may be stale: %s", exc) |
| self._env.sim.set_camera_view(eye=cam_eye, target=cam_target) | ||
| # set the renderer viewport camera view (does not broadcast to visualizers) | ||
| # Kit-specific helper lives in isaaclab_physx | ||
| from isaaclab_physx.renderers.kit_viewport_utils import set_kit_renderer_camera_view |
There was a problem hiding this comment.
🟡 Warning: This introduces a hard import from isaaclab (core) → isaaclab_physx (backend-specific package). ViewportCameraController lives in the core package but now depends on isaaclab_physx.renderers.kit_viewport_utils at call time.
This is mitigated by the has_kit() guard at the env level (the controller is only created when Kit is present), but the import will raise ModuleNotFoundError in a pure-Newton deployment that somehow reaches this code path. The previous self._env.sim.set_camera_view() was backend-agnostic.
Consider wrapping with a fallback:
try:
from isaaclab_physx.renderers.kit_viewport_utils import set_kit_renderer_camera_view
set_kit_renderer_camera_view(eye=cam_eye, target=cam_target, camera_prim_path=self.cfg.cam_prim_path)
except (ImportError, ModuleNotFoundError):
pass # Kit-only; Newton-only deployments skip viewport cameraThere was a problem hiding this comment.
is this whole class applicable to other visualizer/sensors? I think we may want to look at decoupling this class between the controller and viewport logic so that it can be applied to other visualizers and avoids backend-specific references.
| self._viewport_window = None | ||
| self._viewport_api = None | ||
| self._controlled_camera_path = "/OmniverseKit_Persp" if self.cfg.enable_visualizer_cam else None | ||
| if not self.cfg.enable_visualizer_cam: |
There was a problem hiding this comment.
🔵 Suggestion: In the headless path, _controlled_camera_path is set to /OmniverseKit_Persp but _create_and_assign_camera is not called (no dedicated camera prim is created). Then _apply_cfg_camera_pose_if_configured() modifies the default perspective camera directly.
If another component (e.g., a render-product annotator or sensor camera) shares /OmniverseKit_Persp, they'd have conflicting camera state. This seems intentional for headless simplicity — worth a brief comment explaining why headless skips dedicated camera prim creation (e.g., "no viewport to bind a dedicated camera to in headless mode").
| """Viewport name to use when :attr:`create_viewport` is True.""" | ||
|
|
||
| create_viewport: bool = False | ||
| create_viewport: bool = True |
There was a problem hiding this comment.
🔵 Suggestion: The default changed from False to True. This is a breaking change for anyone who relied on the old default — the Kit visualizer previously used the active viewport, now it creates a new one. The PR description mentions breaking changes — this should be called out in the CHANGELOG so users know to set create_viewport=False if they want the previous behavior.
| continue | ||
| if viz.is_rendering_paused(): | ||
| # Keep non-Kit visualizer event loops responsive while rendering is paused. | ||
| if not viz.pumps_app_update(): |
There was a problem hiding this comment.
🔵 Suggestion: Good design — calling viz.step(0.0) for non-app-pumping visualizers keeps their GL event loops (keyboard/mouse for Newton/Rerun/Viser resume buttons) responsive while rendering is paused. Kit is correctly skipped since its step() calls app.update() which shouldn't run during pause.
Consider expanding the comment slightly to explain why this matters (e.g., "Newton/Rerun/Viser need step(0.0) to process user input like the Resume button").
|
can you check the test failures? visualizer tests seem to be failing, not sure if other tests are related to the change. |
Its the frozen visualizer test that's been added which is failing on the kit bug. |
Description
Tests
Newton Visualizer
Kit Visualizer
Rerun Visualizer
RTX Renderer
Else
Type of change
Screenshots
Please attach before and after screenshots of the change if applicable.
Checklist
pre-commitchecks with./isaaclab.sh --formatconfig/extension.tomlfileCONTRIBUTORS.mdor my name already exists there